using System;
using System.Collections.Generic;
using System.Reflection;
using CriticalSystems.Instrumentation.Internal;
using PostSharp.CodeModel;
using PostSharp.CodeModel.Collections;
using PostSharp.CodeWeaver;
using PostSharp.Collections;

namespace CriticalSystems.Instrumentation.Weaver
{
    internal class InstrumentationAdvice : IAdvice
    {
        private const string TraceDeclarationSequenceName = "Trace declaration sequence";
        private const string LogDeclarationSequenceName = "Log declaration sequence";
        private const string TraceFieldName = "__$trace$_";
        private const string LogFieldName = "__$log$_";
        private const string TraceFlagsLocalVariableName = "__$flags$_";
        private const string ExceptionLocalVariableName = "__$ex$_";
        private const string LocalExceptionLocalVariableName = "__$ex_{0}$_";
        private const string ObjectsLocalVariableName = "__$objects_{0}$_";
        private const string TextLocalVariableName = "__$text_{0}$_";
        private const string StaticConstructorName = ".cctor";
        private TracingAttribute tracingAttribute;
        private LoggingAttribute loggingAttribute;
        private readonly InstrumentationTask parent;
        private readonly IMethod instrumentationMethod;
        private LocalVariableSymbol flags;

        /// <summary>
        /// Initializes a new <see cref="InstrumentationAdvice"/>. 
        /// </summary>
        public InstrumentationAdvice(InstrumentationTask parent)
        {
            this.parent = parent;
        }

        public InstrumentationAdvice(InstrumentationTask parent, IMethod instrumentationMethod)
        {
            this.parent = parent;
            this.instrumentationMethod = instrumentationMethod;
        }

        #region IAdvice Members
        public int Priority
        {
            get
            {
                return 2;
            }
        }
        public TracingAttribute TracingAttribute
        {
            get { return this.tracingAttribute; }
            set { this.tracingAttribute = value; }
        }

        public LoggingAttribute LoggingAttribute
        {
            get { return this.loggingAttribute; }
            set { this.loggingAttribute = value; }
        }

        public bool RequiresWeave(WeavingContext context)
        {
            return true;
        }

        public void Weave(WeavingContext context, InstructionBlock block)
        {
            switch (context.JoinPoint.JoinPointKind)
            {
                case JoinPointKinds.BeforeMethodBody:
                    WeaveEntry(context, block);
                    break;
                case JoinPointKinds.AfterMethodBodyException:
                    WeaveAbnormalExit(context, block);
                    break;
                case JoinPointKinds.AfterMethodBodySuccess:
                    WeaveNormalExit(context, block);
                    break;
                case JoinPointKinds.InsteadOfCall:
                    WeaveCall(context, block);
                    break;
                default:
                    throw new ArgumentException(
                        string.Format("Unexpected join point kind: {0}", context.JoinPoint.JoinPointKind));
            }
        }
        #endregion

        private void WeaveEntry(WeavingContext context, InstructionBlock block)
        {
            if (this.loggingAttribute != null && (this.loggingAttribute.LoggingType & LoggingType.LogEntry) != 0)
            {
                WeaveLoggingEntry(context, block);
            }
            if (this.tracingAttribute != null)
            {
                WeaveTracingEntry(context, block);
            }
        }

        private void WeaveNormalExit(WeavingContext context, InstructionBlock block)
        {
            if (this.tracingAttribute != null)
            {
                WeaveTracingNormalExit(context, block);
            }
            if (this.loggingAttribute != null && (this.loggingAttribute.LoggingType & LoggingType.LogExit) != 0)
            {
                WeaveLoggingNormalExit(context, block);
            }
        }

        private void WeaveLoggingEntry(WeavingContext context, InstructionBlock block)
        {
            Utilities.DebugWriteLine("Weaving logging entry for {0}.{1}", context.Method.DeclaringType.Name, context.Method.Name);
            IField logField = GetLogField(context);
            InstructionSequence entrySequence = context.Method.MethodBody.CreateInstructionSequence();
            entrySequence.Comment = "Entry sequence";
            if (context.Method.Name == StaticConstructorName)
            {
                InstructionSequence sequence;
                Utilities.DumpBlock(context.Method.MethodBody.RootInstructionBlock, 0, true, false);
                block = FindBlockWithSequence(context.Method.MethodBody.RootInstructionBlock, LogDeclarationSequenceName, out sequence);
                block.AddInstructionSequence(entrySequence, NodePosition.After, sequence);
            }
            else
            {
                block.AddInstructionSequence(entrySequence, NodePosition.Before, null);
            }

            context.InstructionWriter.AttachInstructionSequence(entrySequence);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, logField);
            context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, context.Method.Name);
            context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Callvirt, this.parent.LogInMethod);
            context.InstructionWriter.DetachInstructionSequence();
        }

        private void WeaveTracingEntry(WeavingContext context, InstructionBlock block)
        {
            Utilities.DebugWriteLine("Weaving tracing entry for {0}.{1}", context.Method.DeclaringType.Name, context.Method.Name);
            IField traceField = GetTraceField(context);
            InstructionSequence entrySequence = context.Method.MethodBody.CreateInstructionSequence();
            entrySequence.Comment = "Entry sequence";
            if (context.Method.Name == StaticConstructorName)
            {
                InstructionSequence sequence;
                Utilities.DumpBlock(context.Method.MethodBody.RootInstructionBlock, 0, true, false);
                block = FindBlockWithSequence(context.Method.MethodBody.RootInstructionBlock, TraceDeclarationSequenceName, out sequence);
                block.AddInstructionSequence(entrySequence, NodePosition.After, sequence);
            }
            else
            {
                block.AddInstructionSequence(entrySequence, NodePosition.Before, null);
            }
            
            context.InstructionWriter.AttachInstructionSequence(entrySequence);
            context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, traceField);
            context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Callvirt, this.parent.TracingFlagMethod);
            this.flags = context.Method.MethodBody.RootInstructionBlock.DefineLocalVariable(
                this.parent.Project.Module.FindType(typeof(TracingFlags), BindingOptions.Default), TraceFlagsLocalVariableName);
            context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Stloc_S, this.flags);

            MethodDefDeclaration method = context.Method;
            Utilities.DebugWriteLine("Looking for attributes");
            IList<ParameterDeclaration> parametersToTrace = new List<ParameterDeclaration>();
            int inParameterCount = 0;
            foreach (ParameterDeclaration parameter in context.Method.Parameters)
            {
                if ((parameter.Attributes & (ParameterAttributes.Out | ParameterAttributes.In)) !=
                    ParameterAttributes.Out)
                {
                    inParameterCount++;
                }
            }
            foreach (CustomAttributeDeclaration attr in method.CustomAttributes)
            {
                if (attr.Constructor.DeclaringType.GetTypeDefinition().Name == typeof (TraceAttribute).ToString())
                {
                    Utilities.DebugWriteLine("Found trace attribute");
                    for (int i = 0; i < method.Parameters.Count; i++)
                    {
                        ParameterDeclaration parameter = method.Parameters[i];
                        if ((parameter.Attributes & (ParameterAttributes.Out | ParameterAttributes.In)) !=
                            ParameterAttributes.Out)
                        {
                            parametersToTrace.Add(parameter);
                        }
                    }
                }
            }
            if (parametersToTrace.Count == 0)
            {
                foreach (ParameterDeclaration parameter in context.Method.Parameters)
                {
                    foreach (CustomAttributeDeclaration attr in parameter.CustomAttributes)
                    {
                        if (attr.Constructor.DeclaringType.GetTypeDefinition().Name ==
                            typeof (TraceAttribute).ToString())
                        {
                            if ((parameter.Attributes & (ParameterAttributes.Out | ParameterAttributes.In)) !=
                                ParameterAttributes.Out)
                            {
                                Utilities.DebugWriteLine("Found parameter trace attribute");
                                parametersToTrace.Add(parameter);
                            }
                        }
                    }
                }
            }

            if (inParameterCount == parametersToTrace.Count)
            {
                // Always tracing all parameters
                InstructionSequence endSequence = context.Method.MethodBody.CreateInstructionSequence();
                endSequence.Comment = "End sequence";
                block.AddInstructionSequence(endSequence, NodePosition.After, entrySequence);
                // if ((Tracing.Flags & TracingFlags.TraceEntryExit | TracingFlags.TraceEntryExitValues) != 0)
                context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, this.flags);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 3);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.And);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 0);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.Ceq);
                context.InstructionWriter.EmitBranchingInstruction(OpCodeNumber.Brtrue, endSequence);
                if (parametersToTrace.Count == 0)
                {
                    context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, traceField);
                    context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, context.Method.Name);
                    context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Callvirt, this.parent.TraceInMethod);
                }
                else
                {
                    WeaveVariadicTraceIn(context, parametersToTrace);
                }
            }
            else
            {
                // Different tracing depending on the trace flags.
                InstructionSequence elseSequence = context.Method.MethodBody.CreateInstructionSequence();
                elseSequence.Comment = "Else sequence";
                block.AddInstructionSequence(elseSequence, NodePosition.After, entrySequence);
                InstructionSequence endSequence = context.Method.MethodBody.CreateInstructionSequence();
                endSequence.Comment = "End sequence";
                block.AddInstructionSequence(endSequence, NodePosition.After, elseSequence);
                // if ((Tracing.Flags & TracingFlags.TraceEntryExit) != 0)
                context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, this.flags);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 1);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.And);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 0);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.Ceq);
                context.InstructionWriter.EmitBranchingInstruction(OpCodeNumber.Brtrue, elseSequence);

                context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, traceField);
                context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, context.Method.Name);
                context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Callvirt, this.parent.TraceInMethod);

                context.InstructionWriter.EmitBranchingInstruction(OpCodeNumber.Br, endSequence);
                context.InstructionWriter.DetachInstructionSequence();

                context.InstructionWriter.AttachInstructionSequence(elseSequence);
                context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
                // if ((Tracing.Flags & TracingFlags.TraceEntryExitValues) != 0)
                context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, this.flags);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 2);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.And);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 0);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.Ceq);
                context.InstructionWriter.EmitBranchingInstruction(OpCodeNumber.Brtrue, endSequence);

                parametersToTrace.Clear();
                for (int i = 0; i < method.Parameters.Count; i++)
                {
                    ParameterDeclaration parameter = method.Parameters[i];
                    if ((parameter.Attributes & (ParameterAttributes.Out | ParameterAttributes.In)) !=
                        ParameterAttributes.Out)
                    {
                        parametersToTrace.Add(parameter);
                    }
                }
                WeaveVariadicTraceIn(context, parametersToTrace);
            }
            context.InstructionWriter.DetachInstructionSequence();
        }

        private static InstructionBlock FindBlockWithSequence(InstructionBlock rootBlock, string sequenceName, out InstructionSequence sequence)
        {
            Utilities.DebugWriteLine("FindBlockWithSequence for {0}", sequenceName);
            InstructionBlock block = null;
            if ((sequence = FindSequence(rootBlock, sequenceName)) != null)
            {
                block = rootBlock;
            }
            else
            {
                IEnumerator<InstructionBlock> children = rootBlock.GetChildrenEnumerator(true);
                while (children.MoveNext())
                {
                    if ((block = FindBlockWithSequence(children.Current, sequenceName, out sequence)) != null)
                    {
                        break;
                    }
                }
            }
            if (block != null)
            {
                Utilities.DebugWriteLine("Found block {0} with sequence {1}", block, sequenceName);
            }
            return block;
        }

        private static InstructionSequence FindSequence(InstructionBlock block, string sequenceName)
        {
            Utilities.DebugWriteLine("FindSequence in block {0}", block);
            InstructionSequence seq = null;
            if (block.HasInstructionSequences)
            {
                seq = block.FirstInstructionSequence;
                while (seq != null)
                {
                    Utilities.DebugWriteLine("Sequence: {0}", seq);
                    if (seq.Comment == sequenceName)
                    {
                        break;
                    }
                    seq = seq.NextSiblingSequence;
                }
                if (seq != null)
                {
                    Utilities.DebugWriteLine("Found sequence {0}", sequenceName);
                }
            }
            return seq;
        }

        private void WeaveLoggingNormalExit(WeavingContext context, InstructionBlock block)
        {
            Utilities.DebugWriteLine("Weaving logging exit for {0}.{1}", context.Method.DeclaringType.Name, context.Method.Name);
            IField logField = GetLogField(context);
            InstructionSequence exitSequence = context.Method.MethodBody.CreateInstructionSequence();
            exitSequence.Comment = "Exit sequence";
            block.AddInstructionSequence(exitSequence, NodePosition.Before, null);

            context.InstructionWriter.AttachInstructionSequence(exitSequence);
            context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, logField);
            context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, context.Method.Name);
            context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Callvirt, this.parent.LogOutMethod);
            context.InstructionWriter.DetachInstructionSequence();
        }

        private void WeaveTracingNormalExit(WeavingContext context, InstructionBlock block)
        {
            Utilities.DebugWriteLine("Weaving tracing exit for {0}.{1}", context.Method.DeclaringType.Name, context.Method.Name);
            IField traceField = GetTraceField(context);
            string methodName = context.Method.Name + "()";

            IList<ParameterDeclaration> parametersToTrace = new List<ParameterDeclaration>();
            int outParameterCount = 0;
            MethodDefDeclaration method = context.Method;
            foreach (ParameterDeclaration parameter in method.Parameters)
            {
                if ((parameter.Attributes & ParameterAttributes.Out) == ParameterAttributes.Out)
                {
                    outParameterCount++;
                }
            }
            bool traceReturnValue = false;
            foreach (CustomAttributeDeclaration attr in method.CustomAttributes)
            {
                if (attr.Constructor.DeclaringType.GetTypeDefinition().Name == typeof (TraceAttribute).ToString())
                {
                    Utilities.DebugWriteLine("Found trace attribute");
                    for (int i = 0; i < method.Parameters.Count; i++)
                    {
                        ParameterDeclaration parameter = method.Parameters[i];
                        if ((parameter.Attributes & ParameterAttributes.Out) == ParameterAttributes.Out)
                        {
                            parametersToTrace.Add(parameter);
                        }
                    }
                    traceReturnValue = true;
                }
            }
            if (parametersToTrace.Count == 0)
            {
                foreach (ParameterDeclaration parameter in method.Parameters)
                {
                    foreach (CustomAttributeDeclaration attr in parameter.CustomAttributes)
                    {
                        if (attr.Constructor.DeclaringType.GetTypeDefinition().Name ==
                            typeof (TraceAttribute).ToString())
                        {
                            if ((parameter.Attributes & ParameterAttributes.Out) == ParameterAttributes.Out)
                            {
                                Utilities.DebugWriteLine("Found parameter trace attribute");
                                parametersToTrace.Add(parameter);
                            }
                        }
                    }
                }
                foreach (CustomAttributeDeclaration attr in method.ReturnParameter.CustomAttributes)
                {
                    if (attr.Constructor.DeclaringType.GetTypeDefinition().Name ==
                        typeof (TraceAttribute).ToString())
                    {
                        Utilities.DebugWriteLine("Found return value trace attribute");
                        traceReturnValue = true;
                    }
                }
            }

            bool voidReturn = method.ReturnParameter.ToString() == "void";
            traceReturnValue = voidReturn ? false : traceReturnValue;
            Utilities.DebugWriteLine("void return {0}, trace return {1}, return param {2}", voidReturn, traceReturnValue,
                              method.ReturnParameter);

            InstructionSequence endSequence = context.Method.MethodBody.CreateInstructionSequence();
            endSequence.Comment = "End sequence";
            block.AddInstructionSequence(endSequence, NodePosition.Before, null);
            InstructionSequence elseSequence = context.Method.MethodBody.CreateInstructionSequence();
            elseSequence.Comment = "Else sequence";
            block.AddInstructionSequence(elseSequence, NodePosition.Before, endSequence);
            InstructionSequence exitSequence = context.Method.MethodBody.CreateInstructionSequence();
            exitSequence.Comment = "Exit sequence";
            block.AddInstructionSequence(exitSequence, NodePosition.Before, elseSequence);

            context.InstructionWriter.AttachInstructionSequence(exitSequence);
            context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);

            if ((voidReturn && outParameterCount == 0) ||
                (traceReturnValue && outParameterCount == parametersToTrace.Count))
            {
                // if ((Tracing.Flags & (TracingFlags.TraceEntryExit | TracingFlags.TraceEntryExitValues)) != 0)
                context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, this.flags);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 3);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.And);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 0);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.Ceq);
                context.InstructionWriter.EmitBranchingInstruction(OpCodeNumber.Brtrue, endSequence);

                if (voidReturn && outParameterCount == 0)
                {
                    context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, traceField);
                    context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, methodName);
                    context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Callvirt, this.parent.TraceOutMethod);
                }
                else
                {
                    WeaveVariadicTraceOut(context, parametersToTrace, traceReturnValue);
                }
            }
            else
            {
                // if ((Tracing.Flags & TracingFlags.TraceEntryExit) != 0)
                context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, this.flags);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 1);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.And);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 0);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.Ceq);
                context.InstructionWriter.EmitBranchingInstruction(OpCodeNumber.Brtrue, elseSequence);

                // Tracing.Out(methodName)
                context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, traceField);
                context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, methodName);
                context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, this.parent.TraceOutMethod);
                context.InstructionWriter.EmitBranchingInstruction(OpCodeNumber.Br, endSequence);
                context.InstructionWriter.DetachInstructionSequence();

                context.InstructionWriter.AttachInstructionSequence(elseSequence);
                context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);

                // else if ((Tracing.Flags & TracingFlags.TraceEntryExitValues) != 0)
                context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, this.flags);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 2);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.And);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, 0);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.Ceq);
                context.InstructionWriter.EmitBranchingInstruction(OpCodeNumber.Brtrue, endSequence);

                // Tracing.Out(methodName)
                parametersToTrace.Clear();
                foreach (ParameterDeclaration parameter in context.Method.Parameters)
                {
                    if ((parameter.Attributes & ParameterAttributes.Out) != 0)
                    {
                        parametersToTrace.Add(parameter);
                    }
                }
                traceReturnValue = voidReturn ? false : true;
                WeaveVariadicTraceOut(context, parametersToTrace, traceReturnValue);
            }
            context.InstructionWriter.DetachInstructionSequence();
        }

        private void WeaveAbnormalExit(WeavingContext context, InstructionBlock block)
        {
            Utilities.DebugWriteLine("Weaving abnormal exit for {0}.{1}", context.Method.DeclaringType.Name,
                              context.Method.Name);
            if (this.tracingAttribute != null || this.loggingAttribute != null)
            {
                IField traceField = null;
                IField logField = null;
                if (this.tracingAttribute != null)
                {
                    traceField = GetTraceField(context);
                }
                if (this.loggingAttribute != null)
                {
                    logField = GetLogField(context);
                }

                InstructionSequence exitSequence = context.Method.MethodBody.CreateInstructionSequence();
                exitSequence.Comment = "Abnormal exit sequence";
                block.AddInstructionSequence(exitSequence, NodePosition.Before, null);
                context.InstructionWriter.AttachInstructionSequence(exitSequence);
                context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);

                LocalVariableSymbol ex = context.Method.MethodBody.RootInstructionBlock.DefineLocalVariable(
                    this.parent.Project.Module.FindType(typeof(Exception), BindingOptions.Default), ExceptionLocalVariableName);

                context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Stloc_S, ex);

                if (this.loggingAttribute != null)
                {
                    context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, logField);
                    context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, context.Method.Name);
                    context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, ex);
                    context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, this.parent.LogOutExceptionMethod);
                }
                if (this.tracingAttribute != null)
                {
                    context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, traceField);
                    context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, context.Method.Name);
                    context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, ex);
                    context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, this.parent.TraceOutExceptionMethod);
                }

                context.InstructionWriter.EmitInstruction(OpCodeNumber.Rethrow);
                context.InstructionWriter.DetachInstructionSequence();
            }
        }

        private void WeaveCall(WeavingContext context, InstructionBlock block)
        {
            IMethod method = null;
            bool isTracingCall = false;
            if (this.instrumentationMethod == this.parent.TracingInfoMethod)
            {
                method = this.parent.TraceInfoMethod;
                isTracingCall = true;
            }
            else if (this.instrumentationMethod == this.parent.TracingWarningMethod)
            {
                method = this.parent.TraceWarningMethod;
                isTracingCall = true;
            }
            else if (this.instrumentationMethod == this.parent.TracingErrorMethod)
            {
                method = this.parent.TraceErrorMethod;
                isTracingCall = true;
            }
            else if (this.instrumentationMethod == this.parent.TracingErrorExceptionMethod)
            {
                method = this.parent.TraceErrorExceptionMethod;
                isTracingCall = true;
            }
            else if (this.instrumentationMethod == this.parent.LoggingInfoMethod)
            {
                method = this.parent.LogInfoMethod;
            }
            else if (this.instrumentationMethod == this.parent.LoggingWarningMethod)
            {
                method = this.parent.LogWarningMethod;
            }
            else if (this.instrumentationMethod == this.parent.LoggingErrorMethod)
            {
                method = this.parent.LogErrorMethod;
            }
            else if (this.instrumentationMethod == this.parent.LoggingErrorExceptionMethod)
            {
                method = this.parent.LogErrorExceptionMethod;
            }
            Utilities.DebugWriteLine("Weaving {0} call for {1}.{2}", isTracingCall ? "tracing":"logging", context.Method.DeclaringType.Name, context.Method.Name);
            Utilities.DebugWriteLine("Block {0}", block);
            if (method != null)
            {
                IField instrumentationField;
                if (isTracingCall)
                {
                    instrumentationField = GetTraceField(context);
                }
                else
                {
                    instrumentationField = GetLogField(context);
                }
                InstructionSequence traceSequence = context.Method.MethodBody.CreateInstructionSequence();
                traceSequence.Comment = "Instrumentation sequence";
                block.AddInstructionSequence(traceSequence, NodePosition.Before, null);
                context.InstructionWriter.AttachInstructionSequence(traceSequence);
                context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
                if (this.instrumentationMethod == this.parent.TracingErrorExceptionMethod ||
                    this.instrumentationMethod == this.parent.LoggingErrorExceptionMethod)
                {
                    LocalVariableSymbol exceptionArrayVariable =
                        block.DefineLocalVariable(
                            this.parent.Project.Module.FindType(typeof(Exception), BindingOptions.Default), LocalExceptionLocalVariableName);

                    context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Stloc_S, exceptionArrayVariable);
                    context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, instrumentationField);
                    context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, context.Method.Name);
                    context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, exceptionArrayVariable);
                    context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Callvirt, method);
                }
                else
                {
                    LocalVariableSymbol objectArrayVariable =
                        block.DefineLocalVariable(
                            this.parent.Project.Module.FindType(typeof(object[]), BindingOptions.Default), ObjectsLocalVariableName);
                    LocalVariableSymbol textVariable =
                        block.DefineLocalVariable(
                            this.parent.Project.Module.FindType(typeof(string), BindingOptions.Default), TextLocalVariableName);

                    context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Stloc_S, objectArrayVariable);
                    context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Stloc_S, textVariable);
                    context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, instrumentationField);
                    context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, context.Method.Name);
                    context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, textVariable);
                    context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, objectArrayVariable);
                    context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Callvirt, method);
                }
                context.InstructionWriter.DetachInstructionSequence();
            }
        }

        private IField GetLogField(WeavingContext context)
        {
            FieldDefDeclarationCollection fields = context.Method.DeclaringType.Fields;
            IField logField = fields.GetByName(LogFieldName);
            if (logField == null)
            {
                Utilities.DebugWriteLine("Adding log field");
                FieldDefDeclaration field = new FieldDefDeclaration();
                field.Attributes = FieldAttributes.InitOnly | FieldAttributes.Static | FieldAttributes.Private;
                field.FieldType = this.parent.LogType;
                field.Name = LogFieldName;
                fields.Add(field);
                logField = field;

                MethodDefDeclarationCollection methods = context.Method.DeclaringType.Methods;
                MethodDefDeclaration staticConstructor = methods.GetOneByName(StaticConstructorName);
                if (staticConstructor == null)
                {
                    Utilities.DebugWriteLine("Creating static constructor");
                    staticConstructor = new MethodDefDeclaration();
                    staticConstructor.Attributes = MethodAttributes.Private | MethodAttributes.RTSpecialName |
                                                   MethodAttributes.Static | MethodAttributes.SpecialName |
                                                   MethodAttributes.HideBySig;
                    staticConstructor.Name = StaticConstructorName;
                    InstructionBlock block = staticConstructor.MethodBody.CreateInstructionBlock();
                    staticConstructor.MethodBody.RootInstructionBlock = block;
                    methods.Add(staticConstructor);
                    Utilities.DebugWriteLine("Assigning log field in new static constructor for method {0}", context.Method.Name);
                    AssignLogField(context, logField, staticConstructor.MethodBody, staticConstructor.MethodBody.RootInstructionBlock, true);
                }
                else
                {
                    InstructionBlock block = FindSequenceInstructionBlock(staticConstructor.MethodBody.RootInstructionBlock);
                    Utilities.DebugWriteLine("Assigning log field in static constructor for method {0}", context.Method.Name);
                    AssignLogField(context, logField, staticConstructor.MethodBody, block, false);
                    Utilities.DumpBlock(staticConstructor.MethodBody.RootInstructionBlock, 0, true, false);                        
                }
            }
            else
            {
                Utilities.DebugWriteLine("Logging field already defined");
            }
            return logField;
        }


        private IField GetTraceField(WeavingContext context)
        {
            FieldDefDeclarationCollection fields = context.Method.DeclaringType.Fields;
            IField traceField = fields.GetByName(TraceFieldName);
            if (traceField == null)
            {
                Utilities.DebugWriteLine("Adding trace field");
                FieldDefDeclaration field = new FieldDefDeclaration();
                field.Attributes = FieldAttributes.InitOnly | FieldAttributes.Static | FieldAttributes.Private;
                field.FieldType = this.parent.TraceType;
                field.Name = TraceFieldName;
                fields.Add(field);
                traceField = field;

                MethodDefDeclarationCollection methods = context.Method.DeclaringType.Methods;
                MethodDefDeclaration staticConstructor = methods.GetOneByName(StaticConstructorName);
                if (staticConstructor == null)
                {
                    Utilities.DebugWriteLine("Creating static constructor");
                    staticConstructor = new MethodDefDeclaration();
                    staticConstructor.Attributes = MethodAttributes.Private | MethodAttributes.RTSpecialName |
                                                   MethodAttributes.Static | MethodAttributes.SpecialName |
                                                   MethodAttributes.HideBySig;
                    staticConstructor.Name = StaticConstructorName;
                    InstructionBlock block = staticConstructor.MethodBody.CreateInstructionBlock();
                    staticConstructor.MethodBody.RootInstructionBlock = block;
                    methods.Add(staticConstructor);
                    Utilities.DebugWriteLine("Assigning trace field in new static constructor for method {0}", context.Method.Name);
                    AssignTraceField(context, traceField, staticConstructor.MethodBody, staticConstructor.MethodBody.RootInstructionBlock, true);
                }
                else
                {
                    InstructionBlock block = FindSequenceInstructionBlock(staticConstructor.MethodBody.RootInstructionBlock);
                    Utilities.DebugWriteLine("Assigning trace field in static constructor for method {0}", context.Method.Name);
                    AssignTraceField(context, traceField, staticConstructor.MethodBody, block, false);
                    Utilities.DumpBlock(staticConstructor.MethodBody.RootInstructionBlock, 0, true, false);
                }
            }
            else
            {
                Utilities.DebugWriteLine("Tracing field already defined");
            }
            return traceField;
        }

        private static InstructionBlock FindSequenceInstructionBlock(InstructionBlock root)
        {
            InstructionBlock seqBlock;
            if ((seqBlock = FindChildSequenceInstructionBlock(root)) == null)
            {
                seqBlock = root;
            }
            Utilities.DebugWriteLine("FindSequenceInstructionBlock");
            Utilities.DumpBlock(seqBlock, 0, false, false);
            return seqBlock;
        }

        private static InstructionBlock FindChildSequenceInstructionBlock(InstructionBlock parent)
        {
            InstructionBlock seqBlock = null;
            if (parent.HasInstructionSequences)
            {
                seqBlock = parent;
            }
            else if (parent.HasChildrenBlocks)
            {
                IEnumerator<InstructionBlock> children = parent.GetChildrenEnumerator();
                while (children.MoveNext())
                {
                    InstructionBlock block = FindChildSequenceInstructionBlock(children.Current);
                    if (block != null)
                    {
                        seqBlock = block;
                        break;
                    }
                }                
            }
            return seqBlock;
        }

        private void AssignTraceField(WeavingContext context, IField traceField, MethodBodyDeclaration body, InstructionBlock block, bool ret)
        {
            InstructionSequence declarationSequence = body.CreateInstructionSequence();
            declarationSequence.Comment = TraceDeclarationSequenceName;
            block.AddInstructionSequence(declarationSequence, NodePosition.Before, null);
            context.InstructionWriter.AttachInstructionSequence(declarationSequence);
            context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
            context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, context.Method.DeclaringType.Name);
            context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Newobj, this.parent.TraceConstructor);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Stsfld, traceField);
            if (ret)
            {
                context.InstructionWriter.EmitInstruction(OpCodeNumber.Ret);
            }
            context.InstructionWriter.DetachInstructionSequence();
        }


        private void AssignLogField(WeavingContext context, IField logField, MethodBodyDeclaration body, InstructionBlock block, bool ret)
        {
            InstructionSequence declarationSequence = body.CreateInstructionSequence();
            declarationSequence.Comment = LogDeclarationSequenceName;
            block.AddInstructionSequence(declarationSequence, NodePosition.Before, null);
            context.InstructionWriter.AttachInstructionSequence(declarationSequence);
            context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
            context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, context.Method.DeclaringType.DeclaringAssembly.Name);
            context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, context.Method.DeclaringType.Name);
            context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Newobj, this.parent.LogConstructor);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Stsfld, logField);
            if (ret)
            {
                context.InstructionWriter.EmitInstruction(OpCodeNumber.Ret);
            }
            context.InstructionWriter.DetachInstructionSequence();
        }

        private void WeaveVariadicTraceIn(WeavingContext context, IList<ParameterDeclaration> parametersToTrace)
        {
            IField traceField = GetTraceField(context);
            string signature = string.Empty;
            bool first = true;
            int i = 0;
            foreach (ParameterDeclaration parameter in context.Method.Parameters)
            {
                if (!first)
                {
                    signature += ", ";
                }
                first = false;
                signature += parameter.ParameterType + " " + parameter.Name;
                if (parametersToTrace.Contains(parameter))
                {
                    signature += string.Format(@" = {{{0}}}", i++);
                }
            }
            string methodNameWithPlaceholders = string.Format("{0}({1}) : {2}", context.Method.Name, signature,
                                                              context.Method.ReturnParameter.ParameterType);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, traceField);
            context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, methodNameWithPlaceholders);
            MakeArrayOfArguments(context, parametersToTrace);
            context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Callvirt, this.parent.TraceInMethodVariadic);
        }

        private void MakeArrayOfArguments(WeavingContext context, IList<ParameterDeclaration> parameters)
        {
            // This item is obfuscated and can not be translated.
            int argOffset = ((context.Method.Attributes & MethodAttributes.Static) != MethodAttributes.ReuseSlot)
                                ? 0
                                : 1;
            if (parameters.Count > 0)
            {
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, parameters.Count);
                context.InstructionWriter.EmitInstructionType(OpCodeNumber.Newarr,
                                                              this.parent.Project.Module.Cache.GetIntrinsicBoxedType(
                                                                  IntrinsicType.Object));
                for (int i = 0; i < parameters.Count; i++)
                {
                    ParameterDeclaration parameter = parameters[i];
                    //NullableBool bool1 = NullableBool.op_LogicalNot(declaration.ParameterType.BelongsToClassification(TypeClassifications.Pointer));
                    if ((parameter.Attributes & (ParameterAttributes.Out | ParameterAttributes.In)) !=
                        ParameterAttributes.Out)
                    {
                        context.InstructionWriter.EmitInstruction(OpCodeNumber.Dup);
                        context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, i);
                        context.InstructionWriter.EmitInstructionInt16(OpCodeNumber.Ldarg,
                                                                       (short) (parameter.Ordinal + argOffset));
                        context.WeavingHelper.ToObject(parameter.ParameterType, context.InstructionWriter);
                        context.InstructionWriter.EmitInstruction(OpCodeNumber.Stelem_Ref);
                    }
                }
            }
        }

        private void WeaveVariadicTraceOut(WeavingContext context, IList<ParameterDeclaration> parametersToTrace,
                                           bool traceReturnValue)
        {
            IField traceField = GetTraceField(context);
            string methodName = context.Method.Name;
            string signature = string.Empty;
            bool first = true;
            int i = 0;
            foreach (ParameterDeclaration parameter in context.Method.Parameters)
            {
                if (parametersToTrace.Contains(parameter))
                {
                    if (!first)
                    {
                        signature += ", ";
                    }
                    first = false;
                    signature += parameter.ParameterType + " " + parameter.Name;
                    signature += string.Format(@" = {{{0}}}", i++);
                }
            }
            if (traceReturnValue)
            {
                string methodNameWithPlaceholders = string.Format("{0}({1}) = {{{2}}}", methodName, signature, i);
                context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, traceField);
                context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, methodNameWithPlaceholders);
                MakeArrayOfReturnArguments(context, parametersToTrace, true);
                context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, this.parent.TraceOutMethodVariadic);
            }
            else
            {
                string methodNameWithPlaceholders = string.Format("{0}({1})", methodName, signature);
                context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldsfld, traceField);
                context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, methodNameWithPlaceholders);
                MakeArrayOfReturnArguments(context, parametersToTrace, false);
                context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, this.parent.TraceOutMethodVariadic);
            }
        }

        private void MakeArrayOfReturnArguments(WeavingContext context, IList<ParameterDeclaration> parameters,
                                                bool includeReturnValue)
        {
            int argOffset = ((context.Method.Attributes & MethodAttributes.Static) != MethodAttributes.ReuseSlot)
                                ? 0
                                : 1;
            context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4,
                                                           includeReturnValue ? parameters.Count + 1 : parameters.Count);
            context.InstructionWriter.EmitInstructionType(OpCodeNumber.Newarr,
                                                          this.parent.Project.Module.Cache.GetIntrinsicBoxedType(
                                                              IntrinsicType.Object));
            for (int i = 0; i < parameters.Count; i++)
            {
                ParameterDeclaration parameter = parameters[i];
                if ((parameter.Attributes & ParameterAttributes.Out) != 0)
                {
                    context.InstructionWriter.EmitInstruction(OpCodeNumber.Dup);
                    context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, i);
                    context.InstructionWriter.EmitInstructionInt16(OpCodeNumber.Ldarg,
                                                                   (short) (parameter.Ordinal + argOffset));
                    context.WeavingHelper.ToObject(parameter.ParameterType, context.InstructionWriter);
                    context.InstructionWriter.EmitInstruction(OpCodeNumber.Stelem_Ref);
                }
            }
            if (includeReturnValue)
            {
                context.InstructionWriter.EmitInstruction(OpCodeNumber.Dup);
                context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, parameters.Count);
                context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, context.ReturnValueVariable);
                context.WeavingHelper.ToObject(context.Method.ReturnParameter.ParameterType, context.InstructionWriter);
                context.InstructionWriter.EmitInstruction(OpCodeNumber.Stelem_Ref);
            }
        }
    }
}